Skip to content

fix(web): preserve unsent user-input answers across thread switches - #4592

Open
ipanasenko wants to merge 3 commits into
pingdotgg:mainfrom
ipanasenko:fix/preserve-pending-user-input-draft
Open

fix(web): preserve unsent user-input answers across thread switches#4592
ipanasenko wants to merge 3 commits into
pingdotgg:mainfrom
ipanasenko:fix/preserve-pending-user-input-draft

Conversation

@ipanasenko

@ipanasenko ipanasenko commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Problem

A typed-but-unsent answer to a provider's user-input question is lost as soon as you switch to another thread and come back.

Screen.Recording.2026-07-26.at.11.30.20.PM.mov

Cause

The draft answers lived in ChatView component state — pendingUserInputAnswersByRequestId / pendingUserInputQuestionIndexByRequestId. The router creates a new match per $threadId, so a thread switch unmounts ChatView and throws that state away. Nothing was persisted.

Change

  • New apps/web/src/pendingUserInputDraftStore.ts: a zustand store persisted to localStorage (t3code:pending-user-input-drafts:v1, same pattern as terminalUiStateStore) holding draft answers and the active question index keyed by requestId. Has a 50-request retention cap so abandoned drafts don't accumulate.
  • ChatView reads/writes that store instead of local useState. Answer logic (togglePendingUserInputOptionSelection, setPendingUserInputCustomAnswer) is unchanged — the store just takes an updater.
  • The draft is cleared once the answer submits successfully; a failed submit keeps it so the user can retry.

On returning to the thread, ChatComposer's existing hydration effect restores the editor text from activePendingProgress.customAnswer, so typed text, selected options, and multi-question position all come back. Drafts now also survive a reload.

Testing

  • tsgo --noEmit on apps/web — clean
  • vp lint on the touched files — clean
  • vp test run --project unit — 8 new store tests plus existing pendingUserInput, ChatView.logic, AppRoot suites (52 tests) pass

Also verified manually live in the app against a real pending question.

Note

Preserve unsent user-input draft answers across thread switches in ChatView

  • Replaces component-local state in ChatViewContent with a persisted zustand store (pendingUserInputDraftStore.ts) that retains draft answers and the active question index per request id.
  • Drafts survive component unmounts (e.g. switching threads) and are cleared only when the corresponding request is no longer present in derivePendingUserInputs.
  • The store normalizes question indexes, safely handles prototype-shadowing keys (__proto__) via object spread and JSON serialization, and evicts the oldest drafts beyond a cap of 50 retained request ids.
  • Behavioral Change: draft state previously reset on every thread switch; it now persists across navigations and is only cleared when the request completes.

Macroscope summarized 753fff0.


Note

Low Risk
Scoped to client-side draft UX in localStorage with bounded retention; no auth, API, or server behavior changes.

Overview
Unsent answers to provider user-input prompts now survive thread navigation and page reloads by moving draft state out of ChatView into a persisted Zustand store keyed by requestId.

ChatView no longer keeps pendingUserInputAnswersByRequestId / pendingUserInputQuestionIndexByRequestId in component state (which was dropped on thread unmount). It reads and updates pendingUserInputDraftStore, which persists to localStorage (t3code:pending-user-input-drafts:v1) and retains up to 50 request drafts with oldest-first eviction. Draft cleanup runs when a requestId falls off the pending user-input list (not immediately on submit), so the panel does not flash empty while the request is still pending until user-input.resolved arrives.

The new store reuses existing answer helpers via updaters, normalizes question index, and builds answer maps safely for edge keys like __proto__. Eight unit tests cover isolation, retention, and clearing behavior.

Reviewed by Cursor Bugbot for commit 753fff0. Bugbot is set up for automated code reviews on this repo. Configure here.

Draft answers to a provider's user-input request lived in ChatView state,
keyed by requestId. The router creates a new match per thread, so switching
threads unmounted ChatView and discarded a typed but unsent answer.

Move the drafts into a persisted zustand store keyed by requestId, so they
outlive the component (and a reload). The draft is cleared once the answer
is submitted successfully; failures keep it so the user can retry.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2cc7a853-8712-4b93-94af-9c08bb031656

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 26, 2026
Comment thread apps/web/src/pendingUserInputDraftStore.ts Outdated
`record[key] = value` invokes the prototype setter for a `"__proto__"`
question id instead of creating an own property, so the draft vanished on
serialize. Build the next answers map with a computed key in an object
literal (and rest-strip on removal), both of which define own properties.
@ipanasenko
ipanasenko marked this pull request as ready for review July 26, 2026 22:40
Comment thread apps/web/src/pendingUserInputDraftStore.ts Outdated
Comment thread apps/web/src/components/ChatView.tsx Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new persisted state store with non-trivial cleanup logic. There is an unresolved high-severity comment identifying a potential bug where thread-switching may incorrectly clear drafts from other threads.

You can customize Macroscope's approvability policy. Learn more.

Two follow-ups from review:

- Clearing the draft on submit success blanked the panel during the window
  before the `user-input.resolved` activity arrives, since the request is
  still listed as pending until then. Clear when a request drops out of the
  pending list instead, which also covers cancelled and stale-failed requests.
- Answers and the question index were evicted independently, so the retention
  cap could keep one half of a draft without the other. Store both in a single
  per-request record so eviction is atomic.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 753fff0. Configure here.

}
}
seenPendingUserInputRequestIdsRef.current = currentRequestIds;
}, [pendingUserInputs]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thread switch clears other drafts

High Severity

The pending-input cleanup effect compares the previous pendingUserInputs snapshot to the current thread’s list, but seenPendingUserInputRequestIdsRef is not reset when routeThreadKey / threadId changes. Navigating to another thread (same mounted ChatView) treats the prior thread’s pending requestIds as removed and calls clearPendingUserInputDraft, wiping persisted drafts while those requests are still open on the original thread.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 753fff0. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant